//******************************************************************************
//
// Descriptive Name = component of EntLibContrib for DB2 .NET provider
//
// Function = DB2 Client Enablement
//
// (c) Copyright IBM Corp 2008
//
//******************************************************************************
//==============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Data Access Application Block
//==============================================================================

using System;
using System.Data;
using System.Data.Common;
using System.Security.Permissions;
using System.Xml;
using System.Globalization;
using System.Transactions;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Unity;
using Microsoft.Practices.EnterpriseLibrary.Data;
using Microsoft.Practices.EnterpriseLibrary.Data.Configuration;
using EntLibContrib.Data.DB2.Configuration.Unity;
using EntLibContrib.Data.DB2.Properties;
using IBM.Data.DB2;

namespace EntLibContrib.Data.DB2
{
	/// <summary>
	/// Represents a IBM Data Server database.
	/// </summary>
	/// <remarks>
	/// Internally uses IBM Data Server .NET provider to connect to the database.
	/// </remarks>
	[DB2Permission(SecurityAction.Demand)]
	[DatabaseAssembler(typeof(DB2DatabaseAssembler))]
	[ContainerPolicyCreator(typeof(DB2DatabasePolicyCreator))]
	public class DB2Database : Database
	{
		#region Constants
		/// <summary>
		/// The parameter token used to delimit parameters for the IBM DB2 database.
		/// </summary>
		protected const char ParameterToken = '@';
		#endregion

		#region Construction
		/// <summary>
		/// Initializes a new instance of the <see cref="DB2Database"/> class with a connection string.
		/// </summary>
		/// <param name="connectionString">The connection string.</param>
		public DB2Database(string connectionString)
			: base(connectionString, DB2Factory.Instance)
		{
		}
		#endregion

		#region Public methods
		/// <summary>
		/// Adds a new In <see cref="DbParameter"/> object to the given <paramref name="command"/>.
		/// </summary>
		/// <param name="command">The command to add the in parameter.</param>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DB2Type"/> values.</param>
		/// <remarks>
		/// This version of the method is used when you can have the same parameter object multiple times with different values.
		/// </remarks>
		public void AddInParameter(DbCommand command,
															string name,
															DB2Type dbType)
		{
			AddParameter(command, name, dbType, ParameterDirection.Input, String.Empty, DataRowVersion.Default, null);
		}

		/// <summary>
		/// Adds a new In <see cref="DbParameter"/> object to the given <paramref name="command"/>.
		/// </summary>
		/// <param name="command">The commmand to add the parameter.</param>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DB2Type"/> values.</param>
		/// <param name="value">The value of the parameter.</param>
		public void AddInParameter(DbCommand command,
															string name,
															DB2Type dbType,
															object value)
		{
			AddParameter(command, name, dbType, ParameterDirection.Input, String.Empty, DataRowVersion.Default, value);
		}

		/// <summary>
		/// Adds a new In <see cref="DbParameter"/> object to the given <paramref name="command"/>.
		/// </summary>
		/// <param name="command">The command to add the parameter.</param>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DB2Type"/> values.</param>
		/// <param name="sourceColumn">The name of the source column mapped to the DataSet and used for loading or returning the value.</param>
		/// <param name="sourceVersion">One of the <see cref="DataRowVersion"/> values.</param>
		public void AddInParameter(DbCommand command,
															string name,
															DB2Type dbType,
															string sourceColumn,
															DataRowVersion sourceVersion)
		{
			AddParameter(command, name, dbType, 0, ParameterDirection.Input, true, 0, 0, sourceColumn, sourceVersion, null);
		}

		/// <summary>
		/// Adds a new Out <see cref="DbParameter"/> object to the given <paramref name="command"/>.
		/// </summary>
		/// <param name="command">The command to add the out parameter.</param>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DB2Type"/> values.</param>
		/// <param name="size">The maximum size of the data within the column.</param>
		public void AddOutParameter(DbCommand command,
																string name,
																DB2Type dbType,
																int size)
		{
			ParameterDirection direction = ParameterDirection.Output;
			string conns = ConnectionString.ToUpper(CultureInfo.CurrentCulture);
			if (conns.Contains("SERVERTYPE=UNIDATA") && direction == ParameterDirection.Output)
			{
				direction = ParameterDirection.InputOutput;
			}
			// standard set of OutParameter for DB2 and IDS and UniVerse
			AddParameter(command, name, dbType, size, direction, true, 0, 0, String.Empty, DataRowVersion.Default, DBNull.Value);
		}

		/// <summary>
		/// Adds a new instance of a <see cref="DbParameter"/> object to the command.
		/// </summary>
		/// <param name="command">The command to add the parameter.</param>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DbType"/> values.</param>
		/// <param name="size">The maximum size of the data within the column.</param>
		/// <param name="direction">One of the <see cref="ParameterDirection"/> values.</param>
		/// <param name="nullable">A value indicating whether the parameter accepts <see langword="null"/> (<b>Nothing</b> in Visual Basic) values.</param>
		/// <param name="precision">The maximum number of digits used to represent the <paramref name="value"/>.</param>
		/// <param name="scale">The number of decimal places to which <paramref name="value"/> is resolved.</param>
		/// <param name="sourceColumn">The name of the source column mapped to the DataSet and used for loading or returning the <paramref name="value"/>.</param>
		/// <param name="sourceVersion">One of the <see cref="DataRowVersion"/> values.</param>
		/// <param name="value">The value of the parameter.</param>
		public virtual void AddParameter(DbCommand command,
																		string name,
																		DB2Type dbType,
																		int size,
																		ParameterDirection direction,
																		bool nullable,
																		byte precision,
																		byte scale,
																		string sourceColumn,
																		DataRowVersion sourceVersion,
																		object value)
		{
			DbParameter parameter = CreateParameter(name, dbType, size, direction, nullable, precision, scale, sourceColumn, sourceVersion, value);
			command.Parameters.Add(parameter);
		}

		/// <summary>
		/// Adds a new instance of a <see cref="DbParameter"/> object to the command.
		/// </summary>
		/// <param name="command">The command to add the parameter.</param>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DB2Type"/> values.</param>
		/// <param name="direction">One of the <see cref="ParameterDirection"/> values.</param>
		/// <param name="sourceColumn">The name of the source column mapped to the DataSet and used for loading or returning the <paramref name="value"/>.</param>
		/// <param name="sourceVersion">One of the <see cref="DataRowVersion"/> values.</param>
		/// <param name="value">The value of the parameter.</param>
		public void AddParameter(DbCommand command,
														string name,
														DB2Type dbType,
														ParameterDirection direction,
														string sourceColumn,
														DataRowVersion sourceVersion,
														object value)
		{
			AddParameter(command, name, dbType, 0, direction, false, 0, 0, sourceColumn, sourceVersion, value);
		}

		/// <summary>
		/// Configures a given <see cref="DbParameter"/>.
		/// </summary>
		/// <param name="parameter">The parameter.</param>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DB2Type"/> values.</param>
		/// <param name="size">The maximum size of the data within the column.</param>
		/// <param name="direction">One of the <see cref="ParameterDirection"/> values.</param>
		/// <param name="nullable">A value indicating whether the parameter accepts <see langword="null"/> (<b>Nothing</b> in Visual Basic) values.</param>
		/// <param name="precision">The maximum number of digits used to represent the <paramref name="value"/>.</param>
		/// <param name="scale">The number of decimal places to which <paramref name="value"/> is resolved.</param>
		/// <param name="sourceColumn">The name of the source column mapped to the DataSet and used for loading or returning the <paramref name="value"/>.</param>
		/// <param name="sourceVersion">One of the <see cref="DataRowVersion"/> values.</param>
		/// <param name="value">The value of the parameter.</param>
		protected virtual void ConfigureParameter(DB2Parameter parameter,
																							string name,
																							DB2Type dbType,
																							int size,
																							ParameterDirection direction,
																							bool nullable,
																							byte precision,
																							byte scale,
																							string sourceColumn,
																							DataRowVersion sourceVersion,
																							object value)
		{
			parameter.DB2Type = dbType;
			parameter.Size = size;
			parameter.Value = (value == null) ? DBNull.Value : value;
			parameter.Direction = direction;
			parameter.IsNullable = nullable;
			parameter.Precision = precision;
			parameter.Scale = scale;
			parameter.SourceColumn = sourceColumn;
			parameter.SourceVersion = sourceVersion;
		}

		/// <summary>
		/// Adds a new instance of a <see cref="DbParameter"/> object.
		/// </summary>
		/// <param name="name">The name of the parameter.</param>
		/// <param name="dbType">One of the <see cref="DbType"/> values.</param>
		/// <param name="size">The maximum size of the data within the column.</param>
		/// <param name="direction">One of the <see cref="ParameterDirection"/> values.</param>
		/// <param name="nullable">A value indicating whether the parameter accepts <see langword="null"/> (<b>Nothing</b> in Visual Basic) values.</param>
		/// <param name="precision">The maximum number of digits used to represent the <paramref name="value"/>.</param>
		/// <param name="scale">The number of decimal places to which <paramref name="value"/> is resolved.</param>
		/// <param name="sourceColumn">The name of the source column mapped to the DataSet and used for loading or returning the <paramref name="value"/>.</param>
		/// <param name="sourceVersion">One of the <see cref="DataRowVersion"/> values.</param>
		/// <param name="value">The value of the parameter.</param>
		/// <returns></returns>
		protected DbParameter CreateParameter(string name,
																					DB2Type dbType,
																					int size,
																					ParameterDirection direction,
																					bool nullable,
																					byte precision,
																					byte scale,
																					string sourceColumn,
																					DataRowVersion sourceVersion,
																					object value)
		{
			DB2Parameter parameter = CreateParameter(name) as DB2Parameter;
			ConfigureParameter(parameter, name, dbType, size, direction, nullable, precision, scale, sourceColumn, sourceVersion, value);
			return parameter;
		}

		/// <summary>
		/// Retrieves parameter information from the stored procedure specified in the <see cref="DbCommand"/> and
		/// populates the Parameters collection of the specified <see cref="DbCommand"/> object.
		/// </summary>
		/// <param name="discoveryCommand">The <see cref="DbCommand"/> to do the discovery.</param>
		/// <remarks>The <see cref="DbCommand"/> must be a <see cref="DB2Command"/> instance.</remarks>
		protected override void DeriveParameters(DbCommand discoveryCommand)
		{
			DB2CommandBuilder.DeriveParameters((DB2Command)discoveryCommand);
		}

		/// <summary>
		/// Executes the <see cref="DB2Command"/> and returns a new <see cref="XmlReader"/>.
		/// </summary>
		/// <param name="command">The <see cref="DB2Command"/> to execute.</param>
		/// <returns>An <see cref="XmlReader"/> object.</returns>
		/// <remarks>
		/// Unlike other Execute... methods that take a <see cref="DbCommand"/> instance, this method
		/// does not set the command behavior to close the connection when you close the reader.
		/// That means you'll need to close the connection yourself, by calling the
		/// command.Connection.Close() method.
		/// <para>
		/// There is one exception to the rule above. If you're using <see cref="TransactionScope"/> to provide
		/// implicit transactions, you should NOT close the connection on this reader when you're
		/// done. Only close the connection if <see cref="Transaction"/>.Current is null.
		/// </para>
		/// </remarks>
		public XmlReader ExecuteXmlReader(DbCommand command)
		{
			DB2Command db2Command = CheckIfDB2Command(command);

			ConnectionWrapper wrapper = GetOpenConnection(false);
			PrepareCommand(command, wrapper.Connection);
			return DoExecuteXmlReader(db2Command);
		}

		/// <summary>
		/// Executes the <see cref="DB2Command"/> in a transaction and returns a new <see cref="XmlReader"/>.
		/// </summary>
		/// <param name="command">The <see cref="DB2Command"/> to execute.</param>
		/// <param name="transaction">The <see cref="IDbTransaction"/> to execute the command within.</param>
		/// <returns>An <see cref="XmlReader"/> object.</returns>
		/// <remarks>
		/// Unlike other Execute... methods that take a <see cref="DbCommand"/> instance, this method
		/// does not set the command behavior to close the connection when you close the reader.
		/// That means you'll need to close the connection yourself, by calling the
		/// command.Connection.Close() method.
		/// </remarks>
		public XmlReader ExecuteXmlReader(DbCommand command, DbTransaction transaction)
		{
			DB2Command db2Command = CheckIfDB2Command(command);

			PrepareCommand(db2Command, transaction);
			return DoExecuteXmlReader(db2Command);
		}

		/// <summary>
		/// Sets the RowUpdated event for the data adapter.
		/// </summary>
		/// <param name="adapter">The <see cref="DbDataAdapter"/> to set the event.</param>
		protected override void SetUpRowUpdatedEvent(DbDataAdapter adapter)
		{
			((DB2DataAdapter)adapter).RowUpdated += new DB2RowUpdatedEventHandler(OnDB2RowUpdated);
		}

		/// <summary>
		/// Determines if the number of parameters in the command matches the array of parameter values.
		/// </summary>
		/// <param name="command">The <see cref="DbCommand"/> containing the parameters.</param>
		/// <param name="values">The array of parameter values.</param>
		/// <returns>
		/// 	<see langword="true"/> if the number of parameters and values match; otherwise, <see langword="false"/>.
		/// </returns>
		protected override bool SameNumberOfParametersAndValues(DbCommand command, object[] values)
		{
			int returnParameterCount = 1;
			string conns = ConnectionString.ToUpper(CultureInfo.CurrentCulture);

			if (conns.Contains("SERVERTYPE=UNI") && (values.Length == 1) && (command.Parameters.Count == 1))
			{
				// if UniData or UniVerse databases, set to 0 for ExecuteScalar
				returnParameterCount = 0;
			}

			int numberOfParametersToStoredProcedure = command.Parameters.Count - returnParameterCount;
			int numberOfValuesProvidedForStoredProcedure = values.Length;

			return numberOfParametersToStoredProcedure == numberOfValuesProvidedForStoredProcedure;
		}

		/// <summary>
		/// Returns the starting index for parameters in a command.
		/// </summary>
		/// <returns>
		/// The starting index for parameters in a command.
		/// </returns>
		protected override int UserParametersStartIndex()
		{
			string conns = ConnectionString.ToUpper(CultureInfo.CurrentCulture);

			Int16 userparamstartindex = 1;
			if (conns.Contains("SERVERTYPE=UNI"))
			{
				// if UniData or UniVerse databases, this needs to be 0
				userparamstartindex = 0;
			}
			return userparamstartindex;
		}
		#endregion

		#region Public Static Methods
		/// <summary>
		/// Checks if a database command is a DB2 command and converts.
		/// </summary>
		/// <param name="command">The command.</param>
		/// <returns>converted DB2Command</returns>
		public static DB2Command CheckIfDB2Command(DbCommand command)
		{
			DB2Command db2Command = command as DB2Command;
			if (db2Command == null)
				throw new ArgumentException(Resources.ExceptionCommandNotDB2Command, "command");

			return db2Command;
		}
		#endregion

		#region Private Methods
		/// <devdoc>
		/// Execute the actual XML Reader call.
		/// </devdoc>        
		private XmlReader DoExecuteXmlReader(DB2Command db2Command)
		{
			try
			{
				DateTime startTime = DateTime.Now;
				XmlReader reader = db2Command.ExecuteXmlReader();
				instrumentationProvider.FireCommandExecutedEvent(startTime);
				return reader;
			}
			catch (Exception e)
			{
				instrumentationProvider.FireCommandFailedEvent(db2Command.CommandText, ConnectionStringNoCredentials, e);
				throw;
			}
		}

		/// <devdoc>
		/// Listens for the RowUpdate event on a dataadapter to support UpdateBehavior.Continue
		/// </devdoc>
		private void OnDB2RowUpdated(object sender, DB2RowUpdatedEventArgs rowThatCouldNotBeWritten)
		{
			if (rowThatCouldNotBeWritten.RecordsAffected == 0)
			{
				if (rowThatCouldNotBeWritten.Errors != null)
				{
					rowThatCouldNotBeWritten.Row.RowError = Resources.ExceptionMessageUpdateDataSetRowFailure;
					rowThatCouldNotBeWritten.Status = UpdateStatus.SkipCurrentRow;
				}
			}
		}
		#endregion
	}
}
